Модульное тестирование
======================

.. index::
    модульное тестирование

Как известно, любое устройство, на котором работает какая-либо компьютерная программа, содержит программные ошибки.
Программные ошибки поражают компьютерные
от самых простых до самых сложных устройств. Отладка и исправление программных
это один из самых неприятных видов повседневной деятельности программиста.

Причины возникновения ошибок
----------------------------

Существует множество причин, по которым возникают ошибки в программном обеспечении. Ниже приведен неполный
список, начиная с этапа проектирования программы и заканчивая ее кодированием:

* Требования и спецификации программы могут быть неясны.
  То, что программа должна делать на самом деле, может быть неизвестно на этапе проектирования.
* Программист может неправильно понять некоторые требования программыю
* Язык программирования может быть недостаточно выразительным. Учитывая, что
  путаница бывает даже между носителями человеческих языков, то
  неестественный синтаксис и правила языка программирования могут быть причиной
  ошибок.
* Некоторые предположения программиста могут быть неверными. Например.
  например, программист может полагать, что 3.14 будет достаточно точным для представления PI. (TODO)
* Программист может иметь неверную информацию по теме или не иметь ее вовсе. Например,
  например, программист может не знать, что использование переменной с плавающей точкой
  в конкретном логическом выражении будет ненадежным.
* Программа может столкнуться с непредвиденной ситуацией. Например, один из
  файлов каталога может быть удален или переименован, пока программа использует
  файлы этого каталога в цикле foreach.
* Программист может допустить глупую ошибку. Например, имя
  переменной может быть неправильно введено и случайно совпасть с именем другой
  переменной.
* и т. д.

К сожалению, до сих пор не существует методологии разработки программного обеспечения, которая гарантировала бы
что программа всегда будет работать правильно. Это по-прежнему актуальная тема программной инженерии
где перспективные решения появляются каждое десятилетие или около того.


Обнаружение ошибок
------------------

Ошибки в программном обеспечении обнаруживаются на разных этапах жизни программы с помощью
различных типов инструментов и людей. Ниже приведен неполный список моментов, когда ошибка может быть
быть обнаружена, от самых ранних до самых поздних:

* При написании программы
   * Программистом
   * Другим программистом во время парного программирования
   * Компилятором через сообщения компилятора
   * С помощью модульных тестов в процессе сборки программы

* При просмотре кода
   * Инструментами, которые анализируют код во время компиляции
   * Другими программистами во время анализа кода

* При выполнении программы
   * С помощью инструментов, анализирующих выполнение программы во время выполнения (например, с помощью valgrind)
   * Во время QA-тестирования, либо в результате отказа проверок утверждений, либо в результате
     наблюдаемым поведением программы
   * Бета-пользователями перед выпуском программы
   * Конечными пользователями после выпуска программы*

Обнаружение ошибок на самых ранних стадиях позволяет сократить потери денег, времени, а в некоторых случаях и человеческих жизней. Кроме того, выявить причины ошибок, которые были обнаружены
конечными пользователями, сложнее, чем выявить причины ошибок, которые обнаруженны на ранних стадиях разработки.

Модульное тестирование для выявления ошибок
-------------------------------------------

Поскольку программы пишутся программистами, а D - компилируемый язык,
программисты и компилятор всегда будут рядом, чтобы обнаружить ошибки.
Помимо этих двух, самым ранним и отчасти по этой причине наиболее эффективным способом
выявления ошибок является модульное тестирование.
Модульное тестирование - неотъемлемая часть современного программирования. Это наиболее
эффективный метод уменьшения количества ошибок при кодировании. Согласно некоторым
методологиям разработки, код, который не защищен модульными тестами, является кодом с ошибками.
К сожалению, обратное неверно: модульные тесты не гарантируют, что модульный код
код не содержит ошибок. Хотя они очень эффективны, они могут только снизить
риск возникновения ошибок.
Юнит-тестирование также позволяет рефакторить код (т.е. вносить в него улучшения) с легкостью и уверенностью. В противном случае часто случается, что при добавлении новых функций в программу случайно ломается часть существующей функциональности. Ошибки
такого типа называются регрессиями. Без модульного тестирования регрессии иногда
обнаруживаются поздно, во время QA-тестирования будущих релизов, или, что еще хуже, конечными пользователями.
пользователями.
Риск регрессий отбивает у программистов желание рефакторить код,
Иногда они не могут выполнить самые простые улучшения, такие как
исправить имя переменной. Это, в свою очередь, приводит к гниению кода - состоянию, когда
код становится все более и более не поддерживаемым. Например, хотя
некоторые строки кода лучше перенести в новую определенную функцию, чтобы ее можно было
чтобы их можно было вызывать из нескольких мест, страх перед регрессиями заставляет программистов копировать и
вставлять существующие строки в другие места, что приводит к проблеме
дублирования.
Фразы типа "если что-то не сломано, не исправляйте это" связаны со страхом регрессий.
Хотя они кажутся мудрыми, такие рекомендации приводят к
медленному гниению кода и превращению его в неприкасаемый беспорядок.
Современное программирование отвергает такую "мудрость". Напротив, чтобы он
не стал источником ошибок, предполагается, что код будет "
безжалостно переработан". Самым мощным инструментом этого современного подхода является модульное тестирование.
Модульное тестирование предполагает независимое тестирование мельчайших фрагментов кода. Когда
блоки кода тестируются независимо, вероятность наличия ошибок в коде снижается. код более высокого уровня, использующий эти единицы измерения. Когда части работают правильно, более
вероятно, что и целое будет работать правильно.
Модульные тесты предоставляются в виде библиотечных решений на других языках (например, JUnit,
CppUnit, Unittest++ и т.д.) В, модульное тестирование является основной функцией языка.
Спорно, что лучше для модульного тестирования - библиотечное решение или языковая функция. Поскольку D не предоставляет некоторых функций, которые обычно
встречаются в библиотеках модульного тестирования, возможно, стоит также рассмотреть библиотечные решения.
Функции модульного тестирования D так же просты, как вставка проверок assert в
блоки unittest.

Активация модульных тестов
--------------------------

Юнит-тесты не являются частью реального выполнения программы. Они должны быть
активировны только во время разработки программы, когда это явно требуется.
Ключ компилятора dmd, активирующий модульные тесты, -unittest.
Предположим, что программа написана в одном исходном файле с именем deneme.d,ее модульные тесты можно активировать следующей командой:

.. code-block:: console

    $ dmd deneme.d -w -unittest

Когда запускается программа, построенная с помощью ключа -unittest, сначала выполняются ее блоки модульного тестирования.
Только если все модульные тесты пройдут, программа начнёт выполняться с функции main().

.. index::
    unittest

Блоки unittest
--------------

Строки кода, включающие модульные тесты, записываются внутри блоков unittest. Эти
блоки не имеют никакого значения для программы, кроме как содержат
модульные тесты:

.. code-block:: d

    unittest
    {
    /* ... the tests and the code that support them ... */
    }

Хотя блоки unittest могут появляться где угодно, удобно определять их
сразу после кода, который они тестируют.
В качестве примера давайте протестируем функцию, которая возвращает порядковую форму
указанного числа, как в "1-м", "2-м" и т.д. Блок unittest этой функции может
просто содержать инструкции assert, которые сравнивают возвращаемые значения функции
с ожидаемыми значениями. Следующая функция тестируется с четырьмя
различными ожидаемыми результатами функции:

.. code-block:: d

    string ordinal(size_t number)
    {
    // ...
    }

    unittest
    {
        assert(ordinal(1) == "1st");
        assert(ordinal(2) == "2nd");
        assert(ordinal(3) == "3rd");
        assert(ordinal(10) == "10th");
    }

Четыре приведенных выше теста проверяют, что функция работает корректно, по крайней мере, для значений
Четыре приведенных выше теста проверяют, что функция работает корректно, по крайней мере, для значений
1, 2, 3 и 10, путем выполнения четырех отдельных вызовов функции и сравнения
возвращаемых значений с ожидаемыми.
Хотя модульные тесты основаны на проверках assert, блоки unittest могут содержать
любой общий код. Это позволяет провести подготовку перед фактическим запуском тестов или любого другого
другого вспомогательного кода, который может потребоваться тестам. Например, следующий блок
сначала определяет переменную, чтобы уменьшить дублирование кода:

.. code-block:: d

    dstring toFront(dstring str, dchar letter)
    {
    // ...
    }

    unittest
    {
        immutable str = "hello"d;
        assert(toFront(str, 'h') == "hello");
        assert(toFront(str, 'o') == "ohell");
        assert(toFront(str, 'l') == "llheo");
    }

Приведенные выше три проверки assert подтверждают, что функция to Front() работает в соответствии со своей
спецификацией.
Как показывают эти примеры, модульные тесты также полезны в качестве примеров того, как
следует вызывать конкретные функции. Обычно легко получить представление о том, что
делает функция, просто прочитав ее модульные тесты.


Тестирование на наличие исключений
----------------------------------

Обычно некоторый код тестируется на наличие типов исключений, которые он должен или не должен
генерировать при определенных условиях. Модуль std.exception содержит две
функции, которые помогают при тестировании на наличие исключений:
* assertThrown: Гарантирует, что определенный тип исключения генерируется из
выражения
* assertNotThrown: Гарантирует, что определенный тип исключения не генерируется из
выражения
Например, функция, которая требует, чтобы оба ее параметра среза имели одинаковую
длину и чтобы она работала с пустыми срезами, может быть протестирована, как в следующих тестах:


.. code-block:: d

    import std.exception;

    int[] average(int[] a, int[] b)
    {
    // ...
    }

    unittest
    {
    /* Must throw for uneven slices */
        assertThrown(average([1], [1, 2]));
    /* Must not throw for empty slices */
        assertNotThrown(average([], []));
    }

Обычно assertThrown гарантирует, что будет выдано исключение определенного типа
независимо от фактического типа этого исключения. При необходимости он также может протестировать
определенный тип исключения. Аналогично, assertNotThrown гарантирует, что вообще не
генерируется исключение, но ему можно дать указание проверить, не генерируется ли определенный
тип исключения. Конкретные типы исключений указаны в качестве
параметров шаблона для этих функций:

.. code-block:: d

    /* Must throw UnequalLengths for uneven slices */
    assertThrown!UnequalLengths(average([1], [1, 2]));
    /* Must not throw RangeError for empty slices (it may
     * throw other types of exceptions) */
    assertNotThrown!RangeError(average([], []));

Шаблоны мы рассмотрим в следующей главе (стр. 399).
Основная цель этих функций - сделать код более кратким и
удобочитаемым. Например, следующая строка assertThrown является эквивалентом
длинного кода под ней:

.. code-block:: d

    assertThrown(average([1], [1, 2]));
    // ...
    /* The equivalent of the line above */
    {
        auto isThrown = false;
        try
        {
            average([1], [1, 2]);
        }
        catch (Exception exc)
        {
            isThrown = true;
        }
        assert(isThrown);
    }


Разработка, управляемая тестированием
--------------------------------------

Разработка, управляемая тестированием (TDD) - это методология разработки программного обеспечения, которая
предписывает написание модульных тестов перед реализацией функциональности. В TDD основное внимание
уделяется модульному тестированию. Кодирование - это второстепенное действие, благодаря которому тесты проходят успешно.
В соответствии с TDD, описанная выше функция ordinal() сначала может быть реализована
намеренно неправильно:

.. code-block:: d

    import std.string;

    string ordinal(size_t number)
    {
        return ""; // ← intentionally wrong
    }

    unittest
    {
        assert(ordinal(1) == "1st");
        assert(ordinal(2) == "2nd");
        assert(ordinal(3) == "3rd");
        assert(ordinal(10) == "10th");
    }

    void main()
    {
    }

Хотя функция явно неверна, следующим шагом было бы запустить модульные
тесты, чтобы убедиться, что тесты действительно выявляют проблемы с функцией:

.. code-block:: console

    $ dmd deneme.d -w -unittest
    $ ./deneme
    core.exception.AssertError@deneme(10): unittest failure

функция должна быть реализована только после обнаружения сбоя и только для
того, чтобы тесты прошли успешно. Вот только одна реализация, которая проходит тесты:

.. code-block:: d

    import std.string;

    string ordinal(size_t number)
    {
        string suffix;
        switch (number)
        {
        case 1:
            suffix = "st";
            break;
        case 2:
            suffix = "nd";
            break;
        case 3:
            suffix = "rd";
            break;
        default:
            suffix = "th";
            break;
        }
        return format("%s%s", number, suffix);
    }

    unittest
    {
        assert(ordinal(1) == "1st");
        assert(ordinal(2) == "2nd");
        assert(ordinal(3) == "3rd");
        assert(ordinal(10) == "10th");
    }

    void main()
    {
    }

Поскольку приведенная выше реализация действительно проходит модульные тесты, есть основания полагать
, что функция ordinal() верна. При условии гарантии, которую дают тесты,
реализация функции может быть изменена многими способами с
уверенностью.

**Модульные тесты перед исправлением ошибок**

Модульные тесты не являются панацеей; ошибки будут всегда. Если ошибка обнаруживается при
фактическом запуске программы, это может рассматриваться как признак того, что модульные тесты
были неполными. По этой причине лучше сначала написать модульный тест, который
воспроизводит ошибку, и только затем исправить ошибку, чтобы пройти новый тест.
Давайте посмотрим на следующую функцию, которая возвращает написание
порядковой формы числа, указанного в виде dstring:

.. code-block:: d

    import std.exception;
    import std.string;

    dstring ordinalSpelled(dstring number)
    {
        enforce(number.length, "number cannot be empty");
        dstring[dstring] exceptions = [
            "one": "first", "two": "second", "three": "third",
            "five": "fifth", "eight": "eighth", "nine": "ninth",
            "twelve": "twelfth"
        ];
        dstring result;
        if (number in exceptions)
        {
            result = exceptions[number];
        }
        else
        {
            result = number ~ "th";
        }
        return result;
    }

    unittest
    {
        assert(ordinalSpelled("one") == "first");
        assert(ordinalSpelled("two") == "second");
        assert(ordinalSpelled("three") == "third");
        assert(ordinalSpelled("ten") == "tenth");
    }

    void main()
    {
    }

Функция заботится об исключительных вариантах написания и даже включает в себя модульный тест для
этого. Тем не менее, в функции есть ошибка, которую еще предстоит обнаружить:

.. code-block:: d

    import std.stdio;

    void main()
    {
        writefln("He came the %s in the race.",
        ordinalSpelled("twenty"));
    }

Орфографическая ошибка в выходных данных программы вызвана ошибкой в
ordinal Spelled(), которую ее модульные тесты не смогли отследить:

.. code-block:: console

    He came the twentyth in the race.

Хотя легко видеть, что функция выдает неправильное написание
чисел, оканчивающихся на букву y, TDD предписывает, что сначала необходимо написать модульный тест
для воспроизведения ошибки, прежде чем фактически ее исправлять:

.. code-block:: d

    unittest
    {
    // ...
        assert(ordinalSpelled("twenty") == "twentieth");
    }

Благодаря этому улучшению тестов, теперь ошибка в функции обнаруживается
во время разработки:

.. code-block:: console

    core.exception.AssertError@deneme(3274338): unittest failure

Только после этого функцию следует исправить:

.. code-block:: d

    dstring ordinalSpelled(dstring number)
    {
        // ...
        if (number in exceptions)
        {
            result = exceptions[number];
        }
        else
        {
            if (number[$ - 1] == 'y')
            {
                result = number[0 .. $ - 1] ~ "ieth";
            }
            else
            {
                result = number ~ "th";
            }
        }
        return result;
    }


Упражнение
----------

Реализуйте to Front() в соответствии с TDD. Начните с намеренно неполной
реализации, приведенной ниже. Обратите внимание, что модульные тесты завершаются неудачей, и предоставьте
реализацию, которая проходит тесты.

.. code-block:: d

    dstring toFront(dstring str, dchar letter)
    {
        dstring result;
        return result;
    }

    unittest
    {
        immutable str = "hello"d;
        assert(toFront(str, 'h') == "hello");
        assert(toFront(str, 'o') == "ohell");
        assert(toFront(str, 'l') == "llheo");
    }

    void main()
    {
    }
